<?php
/**
 * This file defines the {@see \BF\Config\Value} class.
 *
 * @author     Kado <ulfikado@gmail.com>
 * @category   Beluga PHP5.3+ Framework
 * @package    BF\Core
 * @since      2015-01-28 14:29
 * @subpackage Config
 * @version    0.1
 */

namespace BF\Config
{

   /**
    * This class defines a object, that holds all required infos about a config value.
    *
    * A config value can be defined in 2 different ways:
    *
    * - It can use a strictly typed value. It means only values of the initial used type are allowed. It can not be
    *   changed to a value of a other type.
    * - In other case the config value is initialized a a none strict typed value. Here the value type can be changed
    *   by youre need
    *
    * @property      mixed   $Value    The value.
    * @property-read string  $Name     The config field name.
    * @property-read boolean $IsStrict Can only be used values of the initial used value type?
    * @property-read string  $Type     The name of the value type.
    * @since         v0.1
    */
   class Value implements \Serializable
   {

      # <editor-fold defaultstate="collapsed" desc=" - - -   P R I V A T E   F I E L D S   - - - - - - - - - - - - - - - - - - - - - -">

      private $properties = array(
         'value'    => null,
         'type'     => null,
         'isstrict' => false,
         'name'     => ''
      );

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - >   P U B L I C   C O N S T R U C T O R   - - - - - - - - - - - - - - - - - -">

      /**
       * Inits a new instance.
       *
       * @param  string $name     The config field name.
       * @param  mixed  $value    The value.
       * @param  bool   $isStrict Can only used values of the initial used value type?
       * @param  string $typeName If isStrict is TRUE, you have to define here the name of the value type.
       *                          Otherwise NULL is not allowed!
       * @throws \BF\ArgumentException If no usable name is defined, or if $value is NULL in combination with
       *                               strictly type and undefined type name
       */
      public function __construct( $name = '', $value = null, $isStrict = false, $typeName = null )
      {

         if ( $isStrict && \is_null( $value ) && empty( $typeName ) )
         {
            throw new \BF\ArgumentException(
               'value',
               $value,
               \sprintf(
                  'Could not use NULL in case of a strictly typed value requirement of config value "%s" and not defined default type.',
                  $name
               )
            );
         }
         $this->properties[ 'name' ]     = $name;
         $this->properties[ 'value' ]    = $value;
         $this->properties[ 'isstrict' ] = $isStrict;
         $this->properties[ 'type' ]     = empty( $typeName ) ? \BF\TypeTool::GetTypeName( $value ) : $typeName;

      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   M E T H O D S   - - - - - - - - - - - - - - - - - - - - - -">

      /**
       * Magic getter.
       *
       * @param  string $name The name of the required dynamic property ('name', 'value', 'type' or 'isStrict')
       * @return mixed
       * @throws \BF\ArgumentException If a unknown $name is used.
       */
      public function __get( $name )
      {

         $lowerName = \strtolower( $name );

         if ( \array_key_exists( $lowerName, $this->properties ) )
         {
            return $this->properties[ $lowerName ];
         }

         switch ( \strtolower( $name ) )
         {

            case 'typename':
               return $this->properties[ 'type' ];

            case 'strict':
               return $this->properties[ 'isstrict' ];

            default:
               throw new \BF\ArgumentException(
                  'name',
                  $name,
                  \sprintf( 'Illegal access of a not existing dynamic property of config value "%s"!',
                            $this->properties[ 'name' ] )
               );

         }

      }

      /**
       * Returns the config field name.
       *
       * @return string
       */
      public final function getName()
      {

         return $this->properties[ 'name' ];

      }

      /**
       * Returns the value.
       *
       * @return mixed
       */
      public final function getValue()
      {

         return $this->properties[ 'value' ];

      }

      /**
       * Returns the name of the value type. (e.g. 'string', 'int', etc.)
       *
       * @return string
       */
      public final function getType()
      {

         return $this->properties[ 'type' ];

      }

      /**
       * Returns if only values can used of the initial used value type?
       *
       * @return boolean
       */
      public final function isStrictlyTyped()
      {

         return $this->properties[ 'isstrict' ];

      }

      /**
       * The magic dynamic setter method to write access dynamic properties.
       *
       * @param  string $name  The Property name. (Only value can be changed!)
       * @param  mixed  $value The new value to set.
       * @throws \BF\Exception If 'name', 'type' or 'isstrict' should be changed.
       * @throws \BF\ArgumentException  If $name defines a unknown property.
       */
      public function __set( $name, $value )
      {

         switch ( \strtolower( $name ) )
         {
            case 'value':
               $this->setValue( $value );
               break;

            case 'name':
            case 'type':
            case 'isstrict':
               throw new \BF\Exception(
                  \sprint(
                     '(Name, Type, IsStrict) property of configvalue "%s" is readonly! Changing this property fails!',
                     $this->properties[ 'name' ]
                  )
               );

            default:
               throw new \BF\ArgumentException(
                  'name',
                  $name,
                  \sprintf(
                     'Illegal access of a not existing dynamic property of config value "%s"!',
                     $this->properties[ 'name' ]
                  )
               );

         }

      }

      /**
       * Sets a new value.
       *
       * @param  mixed $value The new value to set.
       * @throws \BF\ArgumentException If the value is not accepted.
       */
      public final function setValue( $value )
      {

         if ( \is_null( $value ) )
         {
            $this->properties[ 'value' ] = null;
            return;
         }

         // Getting the type name
         $type = \BF\TypeTool::GetTypeName( $value );

         if ( ! $this->properties[ 'isstrict' ] )
         {
            // If no strict type are required change the value und typename of current instance
            $this->properties[ 'type' ]  = $type;
            $this->properties[ 'value' ] = $value;
            return;
         }

         if ( $this->properties[ 'type' ] != $type )
         {
            // Changing to a other type is not allowed
            throw new \BF\ArgumentException(
               'type',
               $type,
               \sprintf(
                  'Could not change type of strictly typed config value "%s".',
                  $this->properties[ 'name' ]
               )
            );
         }

         $this->properties[ 'value' ] = $value;

      }

      /**
       * Magic to string cast method. Returns the serialized class properties array.
       *
       * @return string
       */
      public function __toString()
      {

         return \serialize( $this->properties );

      }

      /**
       * Returns all instance data as array. (Keys are 'name', 'type', 'isstrict' and 'value')
       *
       * @return array
       */
      public function toArray()
      {

         return $this->properties;

      }

      # <editor-fold defaultstate="collapsed" desc="IMPLEMENT 'Serializable'">

      public function serialize()
      {
         return \serialize( $this->properties );

      }

      public function unserialize( $serialized )
      {
         if ( empty( $serialized ) )
         {
            throw new \BF\ArgumentException(
               'serialized', $serialized,
               \BF\_0( '_Config', 'Unable to deserialize empty data to a \\BF\\Config\\Value instance!' )
            );
         }
         if ( ! \is_string( $serialized ) )
         {
            throw new \BF\ArgumentException(
               'serialized', $serialized,
               \BF\_0( '_Config',
                       'Unable to deserialize data not defined by a string to a \\BF\\Config\\Value instance!' )
            );
         }
         $array = array();
         try
         {
            $array = \unserialize( $serialized );
         }
         catch ( \Exception $ex )
         {
            $ex = null;
            throw new \BF\ArgumentException(
               'serialized', $serialized,
               \BF\_0( '_Config',
                       'Unable to deserialize data if data do not use not a valid php serialization format!' )
            );
         }
         if ( ! \is_array( $array ) )
         {
            throw new \BF\ArgumentException(
               'serialized', $serialized,
               \BF\_0( '_Config',
                       'Unable to deserialize config value data if data do not match required format!' )
            );
         }
         if ( false === ( $tmp = self::FromArray( $array ) ) )
         {
            throw new \BF\ArgumentException(
               'serialized', $serialized,
               \BF\_0( '_Config',
                       'Unable to deserialize config value data if data do not match required format!' )
            );
         }
         $this->properties = $tmp->properties;
      }

      # </editor-fold>

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - - -">

      /**
       * Inits a new {@see \BF\Config\Value} instance from defined array.
       *
       * The array must contain the following keys ('name', 'type', 'isstrict' and 'value')
       *
       * @param  array $array The associative array.
       * @return \BF\Config\Value Or boolean FALSE
       */
      public static function FromArray( array $array )
      {

         if ( ! isset( $array[ 'value' ] )
           || ! isset( $array[ 'isstrict' ] )
           || ! isset( $array[ 'name' ] )
           || ! isset( $array[ 'type' ] ) )
         {
            return false;
         }

         $value = new \BF\Config\Value( $array[ 'name' ], $array[ 'value' ], $array[ 'isstrict' ], $array[ 'type' ] );

         return $value;

      }

      /**
       * Inits a new {@see \BF\Config\Value} instance from defined string. This only words with a string format,
       * returned by  the __toString() implementation of this class.
       *
       * @param  string $str The string, containing the Instance data of a {@see \BF\Config\Value}.
       * @return \BF\Config\Value Or boolean FALSE, if $str uses a invalid format.
       */
      public static function FromString( $str )
      {

         if ( ! \is_string( $str ) )
         {
            return false;
         }

         if ( \preg_match( '~^a:\d+:\{~', $str ) )
         {
            // $str is maybee a serialized array value
            try
            {
               $array = \unserialize( $str );
               return self::FromArray( $array );
            }
            catch ( \Exception $ex ) { $ex = null; }
         }

         return false;

      }

      # </editor-fold>

   }

}

